#region (c) 2014 THN Solutions LLC. - All rights reserved
/*
Simplified BSD License (BSD)
THN HTTP Stack Library
Copyright (c) 2014, THN Solutions LLC. ( www.ThnSolutions.com )
Author: Nguyễn, M. Hải                 ( www.Minh-Hai.com     )
All rights reserved.

Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:

    * Redistributions of source code must retain the above copyright notice, this 
      list of conditions and the following disclaimer.

    * Redistributions in binary form must reproduce the above copyright notice, 
      this list of conditions and the following disclaimer in the documentation 
      and/or other materials provided with the distribution.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
POSSIBILITY OF SUCH DAMAGE.
*/
#endregion

#region Record of Change
//             							CHANGE HISTORY
// -------------------------------------------------------------------------------------
// |   DATE    | DEVELOPER  | DESCRIPTION                                              |
// -------------------------------------------------------------------------------------
// | 04-Nov-12 | HaiNM      | First creation.                                          |
// -------------------------------------------------------------------------------------
//
#endregion

#region Record of Audit
//             							REVIEW HISTORY
// -------------------------------------------------------------------------------------
// |   DATE    | REVIEWER   | NOTES                                                    |
// -------------------------------------------------------------------------------------
// -------------------------------------------------------------------------------------
//
#endregion

#region Using directives
using System;
using System.Text;

using Thn;
using Thn.IO;
#endregion

namespace Thn.Net.Http
{
    /// <summary>
    /// Abstract base class for HTTP content parsers.
    /// </summary>
#if DEBUG || TESTING
    public
#else
    internal
#endif
 abstract class HttpParserBase
    {
        #region Events

        #region Parse Completed
        /// <summary>
        /// This event is raised after parsing is completed successfully.
        /// </summary>
        public event HttpContentParseCompleted ParseCompleted;
        /// <summary>
        /// Raise event ParseCompleted.
        /// </summary>
        protected void RaiseParseCompleted(IHttpContent content)
        {
            HttpContentParseCompleted handler = ParseCompleted;
            if (handler != null)
            {
                handler(this, content);
            }
        }
        #endregion

        #region Parse Failed
        /// <summary>
        /// This event is raised after parsing could not be completed.
        /// </summary>
        public event HttpContentParseFailed ParseFailed;
        /// <summary>
        /// Raise event ParseFailed.
        /// </summary>
        protected void RaiseParseFailed(IHttpContent content, HttpParserFailureReason reason)
        {
            HttpContentParseFailed handler = ParseFailed;
            if (handler != null)
            {
                handler(this, content, reason);
            }
        }
        #endregion

        #endregion

        #region Fields
        HttpParserState mState = HttpParserState.Ready;
        /// <summary>
        /// Current error.
        /// </summary>
        protected HttpParserFailureReason mError = HttpParserFailureReason.None;
        /// <summary>
        /// Maximum size in byte for reading a single line.
        /// </summary>
        int mMaxLineSize = 10 * 1024;
        /// <summary>
        /// Buffer for reading line.
        /// </summary>
        byte[] mBuffer = new byte[10 * 1024];
        /// <summary>
        /// Maximum number of header lines.
        /// </summary>
        int mMaxHeaderLines = 1024;
        ///// <summary>
        ///// Maximum number of bytes the data section can contain.
        ///// </summary>
        //int mMaxDataSize = 10 * 1024 * 1024;
        /// <summary>
        /// Number of bytes current line occupies.
        /// </summary>
        int mCurrentLineSize = 0;
        /// <summary>
        /// Number of headers read so far.
        /// </summary>
        int mCurrentHeadersCount = 0;
        /// <summary>
        /// Number of bytes left for reading to data section.
        /// </summary>
        int mDataBytesLeft = 0;
        /// <summary>
        /// Current content instance.
        /// </summary>
        IHttpContent mCurrentContent = null;
        HttpParserFailureReason mCurrentReason;
        //IBinaryMessage mCurrentMessage;
        //IContextReader mCurrentReader;

        /// <summary>
        /// Whether events should be raised.
        /// </summary>
        bool mRaiseEvents = true;
        const byte SeparatorCharAsByte = (byte)':';
        #endregion

        #region Properties

        #region Tag
        private object mTag;
        /// <summary>
        /// Gets or sets arbitrary value to be used by developers.
        /// </summary>
        public object Tag
        {
            get { return mTag; }
            set { mTag = value; }
        }
        #endregion

        #endregion

        #region On Create Content
        /// <summary>
        /// Create a new instance of <see cref="IHttpContent"/>.
        /// </summary>
        protected abstract IHttpContent OnCreateContent();
        #endregion

        #region On Process First Line
        /// <summary>
        /// This method is raised the very first line reading is completed.
        /// </summary>        
        /// <param name="buffer">The buffer to read line data from.</param>
        /// <param name="size">Number of bytes the line occupies.</param>
        /// <param name="content">The content object for saving parsed data to.</param>
        protected abstract HttpParserFailureReason OnProcessFirstLine(byte[] buffer, int size, IContextReader reader, IHttpContent content);
        #endregion

        #region On Get Data Size
        /// <summary>
        /// Get number of bytes the data section occupies (if any).
        /// </summary>
        /// <param name="content">The content to get size for.</param>
        /// <returns>Positive value means has data. Zero means no data. Negative means has data but size is unknown.</returns>
        protected abstract int OnGetDataSize(IHttpContent content);
        #endregion

        #region On Process Header Line
        /// <summary>
        /// This method is raised when a header line reading is completed.
        /// </summary>        
        /// <param name="buffer">The buffer to read line data from.</param>
        /// <param name="size">Number of bytes the line occupies.</param>
        /// <param name="content">The content object for saving parsed data to.</param>
        protected virtual HttpParserFailureReason OnProcessHeaderLine(byte[] buffer, int size, IContextReader reader, IHttpContent content)
        {
            //Log.Debug("ProcessHeaderLine - size: {0}", size);
            HttpParserFailureReason result = HttpParserFailureReason.None;

            if (size < 1)
            {
                #region Empty line -> Headers section is completed -> read data

                #region Tracing
#if DEBUG || TESTING
                if (NetSettings.TraceHttpParsing)
                {
                    Log.Debug("OnProcessHeaderLine - Empty line found");
                }
#endif
                #endregion

                ChangeState(HttpParserState.HeadersCompleted, reader, content);

                #endregion
            }
            else if (mCurrentHeadersCount < mMaxHeaderLines)
            {
                try
                {
                    #region Process header
                    int idx = buffer.IndexOf(SeparatorCharAsByte, 0, size);
                    if (idx > -1 && idx < size - 2)
                    {
#if SILVERLIGHT
                        //SILVERLIGHT 4.0 does not support ASCII
                        string headerName = Encoding.UTF8.GetString(buffer, 0, idx);
                        idx++;
                        string headerValue = Encoding.UTF8.GetString(buffer, idx, size - idx).Trim();
#else
                        string headerName = Encoding.ASCII.GetString(buffer, 0, idx);
                        idx++;
                        string headerValue = Encoding.ASCII.GetString(buffer, idx, size - idx).Trim();
#endif
                        content.Headers[headerName] = headerValue;

                        #region Tracing
#if DEBUG || TESTING
                        if (NetSettings.TraceHttpParsing)
                        {
                            Log.Debug("OnProcessHeaderLine - Found Header: {0} - Value: {1}", headerName, headerValue);
                        }
#endif
                        #endregion

                        #region Read next header line

                        mCurrentHeadersCount++;
                        mCurrentLineSize = 0;
                        mState = HttpParserState.ReadingHeader;

                        #endregion
                    }
                    else
                    {
                        result = HttpParserFailureReason.InvalidHeader;
                    }
                    #endregion
                }
                catch (Exception ex)
                {
                    result = HttpParserFailureReason.Unspecified;
                    #region Tracing
                    if (NetSettings.PublishError)
                    {
                        Log.Error("Cannot parse header line", ex);
                    }
                    #endregion
                }
            }
            else
            {
                #region Too many headers read -> fail

                mError = HttpParserFailureReason.TooManyHeaderLines;

                #endregion
            }

            #region Error handling
            if (result != HttpParserFailureReason.None)
            {
                ChangeState(HttpParserState.Error, reader, content);
            }
            #region Defensive tracing
            else
            {
                //no error, move on
            }
            #endregion

            #endregion

            return result;
        }
        #endregion

        #region Process - Reading First Line
        /// <summary>
        /// Read data for processing the very first HTTP line until end of line is found.
        /// </summary>
        void Process_ReadingFirstLine(IContextReader reader, IHttpContent content)
        {
            #region Trace
            //Log.Debug("Process_ReadingFirstLine - Available: {0:#,##}", reader.Available);
            #endregion

            while (reader.Available > 0)
            {
                byte b = reader.ReadByte();
                if (b == 10)
                {
                    #region End of line found, complete first line
                    //Log.Information("Process_ReadingFirstLine - Found End of Line {0}", b);

                    //remove the carrier return
                    //if (mCurrentLineSize > 0 && mBuffer[mCurrentLineSize] == 13) mCurrentLineSize--;

                    //HttpParserFailureReason result = OnProcessFirstLine(mBuffer, mCurrentLineSize, reader, mCurrentContent);
                    HttpParserFailureReason result = OnProcessFirstLine(mBuffer, mCurrentLineSize, reader, content);

                    if (result == HttpParserFailureReason.None)
                    {//success -> move to reading header
                        ChangeState(HttpParserState.ReadingHeader, reader, content);
                    }
                    else
                    {//error
                        mError = result;
                        ChangeState(HttpParserState.Error, reader, content);
                    }
                    break;

                    #endregion
                }
                else if (b == 13)
                {
                    //deliberately skip
                }
                else
                {
                    #region Other character, keep reading
                    //Log.Debug("Other character {0}", b);

                    mBuffer[mCurrentLineSize] = b;
                    mCurrentLineSize++;
                    if (mCurrentLineSize > mMaxLineSize)
                    {
                        mError = HttpParserFailureReason.HttpLineTooLong;
                        ChangeState(HttpParserState.Error, reader, content);
                        break;
                    }
                    #region Defensive tracing
                    else
                    {
                        //safe, move on
                    }
                    #endregion

                    #endregion
                }
            }
        }
        #endregion

        #region Process - Reading Header
        /// <summary>
        /// Read data for a single header until end of line is found.
        /// </summary>
        void Process_ReadingHeader(IContextReader reader, IHttpContent content)
        {
            #region Trace
            //Log.Debug("Process_ReadingHeader - Available: {0:#,##}", reader.Available);
            #endregion

            while (reader.Available > 0)
            {
                byte b = reader.ReadByte();
                //Log.Debug("{0} - {1}", b, (char)b);
                if (b == 10)
                {
                    #region End of line found, complete line
                    //Log.Debug("Process_ReadingHeader - Found End of Line {0}", b);

                    //if ((OnProcessHeaderLine(mBuffer, mCurrentLineSize, message, reader, mCurrentContent) != HttpParserFailureReason.None)
                    if ((OnProcessHeaderLine(mBuffer, mCurrentLineSize, reader, content) != HttpParserFailureReason.None)
                        || (mState == HttpParserState.WaitingForDisconnection))
                    {
                        break;
                    }

                    #endregion
                }
                else if (b == 13)
                {
                    //deliberately skip
                }
                else
                {
                    #region Other character, keep reading
                    //Log.Debug("Other character {0}", b);

                    mBuffer[mCurrentLineSize] = b;
                    mCurrentLineSize++;
                    if (mCurrentLineSize > mMaxLineSize)
                    {
                        mError = HttpParserFailureReason.HeaderLineTooLong;
                        ChangeState(HttpParserState.Error, reader, content);

                        break;
                    }
                    #region Defensive tracing
                    else
                    {
                        //safe, move on
                    }
                    #endregion

                    #endregion
                }
            }
        }
        #endregion

        #region Process - Headers Completed
        /// <summary>
        /// Process after all headers are read.
        /// </summary>
        void Process_HeadersCompleted(IContextReader reader, IHttpContent content)
        {
            #region Trace
            //Log.Debug("Process_HeadersCompleted - Available: {0:#,##}", reader.Available);
            #endregion

            //int size = OnGetDataSize(mCurrentContent);
            int size = OnGetDataSize(content);
            if (size > 0)
            {
                #region Has Data section
                mDataBytesLeft = size;
                #region Tracing
#if DEBUG || TESTING
                if (NetSettings.TraceHttpParsing)
                {
                    Log.Debug("Process_HeadersCompleted - Prepare to read {0:#,##} bytes - Available in reader: {1:#,##}", size, reader.Available);
                }
#endif
                #endregion

                ChangeState(HttpParserState.ReadingData, reader, content);
                #endregion
            }
            else if (size < 0)
            {
                #region Size is unknown
                #region Tracing
#if DEBUG || TESTING
                if (NetSettings.TraceHttpParsing)
                {
                    Log.Debug("Process_HeadersCompleted - Has data section but size is unknown - Available in reader: {1:#,#0}", size, reader.Available);
                    Log.Debug("Some client doesn't implement properly -> I'll assume the data is not available and complete the request");
                }
#endif
                #endregion

                ChangeState(HttpParserState.Completed, reader, content);
                #endregion
            }
            else if (mState != HttpParserState.Error)
            {
                #region No Data section
                #region Tracing
#if DEBUG || TESTING
                if (NetSettings.TraceHttpParsing)
                {
                    Log.Debug("Process_HeadersCompleted - No Data Expected - Available in reader: {0:#,##}", reader.Available);
                }
#endif
                #endregion

                ChangeState(HttpParserState.Completed, reader, content);
                #endregion
            }
            else
            {
                //error occurred
                ChangeState(HttpParserState.Error, reader, content);
            }

        }
        #endregion

        #region Process - Reading Data
        /// <summary>
        /// Read data section.
        /// </summary>
        void Process_ReadingData(IContextReader reader, IHttpContent content)
        {
            #region Tracing
#if DEBUG || TESTING
            if (NetSettings.TraceHttpParsing)
            {
                Log.Debug("Process_ReadingData - Available: {0:#,##}", reader.Available);
            }
#endif
            #endregion

            IContextWriter writer = content.Body.Writer;
            int available = (int)reader.Available;
            int bytesToRead = available > mDataBytesLeft ? mDataBytesLeft : available;

            int bufferLength = mBuffer.Length;
            int bytes = bufferLength;
            while (bytesToRead > 0)
            {
                if (bytes > bytesToRead) bytes = bytesToRead;
                reader.ReadBytes(mBuffer, 0, bytes);
                writer.WriteBytes(mBuffer, 0, bytes);

                //next iteration
                bytesToRead -= bytes;
            }

            //prepare for next turn
            mDataBytesLeft -= bytesToRead;

            //finish
            if (bytesToRead <= 0)
            {
                //revert the position to zero for easier later reading
                writer.Seek(0, SeekFrom.Begin);

                ChangeState(HttpParserState.Completed, reader, content);
            }
            #region Defensive tracing
            else
            {
                //not enough yet, keep reading on next turn
            }
            #endregion
        }
        #endregion

        #region Process - Completed
        /// <summary>
        /// Read data for a single header until end of line is found.
        /// </summary>
        void Process_Completed(IContextReader reader, IHttpContent content)
        {
            #region Tracing
#if DEBUG || TESTING
            if (NetSettings.TraceHttpParsing)
            {
                Log.Debug("Completed - Available: {0:#,##}", reader.Available);
            }
#endif
            #endregion

            #region Old codes - 2012 Nov 10 - Only support events
            /*
             
            //raise event
            //content.Body.Writer.Seek(0, SeekFrom.Begin);
            RaiseParseCompleted(content);

            ChangeState(HttpParserState.Ready, reader, content);

            */
            #endregion

            if (mRaiseEvents)
            {
                #region Raise events is required -> raise it

                RaiseParseCompleted(content);

                #endregion
            }
            else
            {
                #region Raise events is not required -> move on



                #endregion
            }
        }
        #endregion

        #region Process - Error
        /// <summary>
        /// Handle error occurred while parsing.
        /// </summary>
        void Process_Error(IContextReader reader, IHttpContent content, HttpParserFailureReason reason)
        {
            #region Trace
            Log.Debug("Error - Reason: {0}", reason);
            #endregion

            #region Old codes - 2012 Nov 10 - Only support events
            /*
            //raise event
            RaiseParseFailed(content, reason);

            */
            #endregion

            if (mRaiseEvents)
            {
                #region Raise events is required -> raise it

                RaiseParseFailed(content, reason);

                #endregion
            }
            else
            {
                #region Raise events is not required -> move on

                mCurrentReason = reason;

                #endregion
            }
        }
        #endregion

        #region Change State
        /// <summary>
        /// Move to a new processing state.
        /// </summary>
        /// <param name="newState">The new state to move to.</param>
        protected void ChangeState(HttpParserState newState, IContextReader reader, IHttpContent content)
        {
            #region Tracing
#if DEBUG || TESTING
            if (NetSettings.TraceHttpParsing)
            {
                Log.Information("Change state from {0} to: {1}", mState, newState);
            }
#endif
            #endregion

            mState = newState;
            ProcessState(reader, content);
        }
        #endregion

        #region Process State
        /// <summary>
        /// Move to a new processing state.
        /// </summary>
        /// <param name="state">The new state to move to.</param>
        void ProcessState(IContextReader reader, IHttpContent content)
        {
            switch (mState)
            {
                case HttpParserState.Ready:
                    ChangeState(HttpParserState.ReadingFirstLine, reader, null);
                    break;
                case HttpParserState.ReadingFirstLine:
                    mCurrentLineSize = 0;
                    mCurrentContent = OnCreateContent();
                    Process_ReadingFirstLine(reader, mCurrentContent);
                    break;
                case HttpParserState.ReadingHeader:
                    mCurrentLineSize = 0;
                    mCurrentHeadersCount = 0;
                    Process_ReadingHeader(reader, content);
                    break;
                case HttpParserState.HeadersCompleted:
                    Process_HeadersCompleted(reader, content);
                    break;
                case HttpParserState.WaitingForDisconnection:
                    //do nothing, wait for disconnection
                    break;
                case HttpParserState.ReadingData:
                    Process_ReadingData(reader, content);
                    break;
                case HttpParserState.Completed:
                    Process_Completed(reader, content);
                    break;
                case HttpParserState.Error:
                    Process_Error(reader, content, mError);
                    break;
            }
        }
        #endregion

        #region Process
        /// <summary>
        /// Tell the parser to process next piece of data. When parsing is completed, event ParseCompleted is raised.
        /// If an error occurs, event ParseFailed is raised.
        /// </summary>
        /// <param name="reader">The reader to retrieve data from.</param>
        public void Process(IContextReader reader)
        {
            mRaiseEvents = true;
            ProcessState(reader, mCurrentContent);
        }
        #endregion

        #region Process And Return If Completed
        /// <summary>
        /// Tell the parser to attempt to process next piece of data. When parsing is completed, the newly parsed content is returned.
        /// This method will not raise event ParseCompleted or ParseFailed. This method will also stops after each success parsing.
        /// </summary>
        /// <param name="reader">The reader to retrieve data from.</param>
        /// <param name="content">The newly parsed content.</param>
        /// <param name="error">The error encountered (if available).</param>
        /// <returns>If parsing is completed, return true. Otherwise, return false.</returns>        
        public bool TryProcess(IContextReader reader, out IHttpContent content, out Exception error)
        {
            bool result = false;
            mRaiseEvents = false;
            error = null;

            #region Pre-processing

            if (mState == HttpParserState.Error || mState == HttpParserState.Completed)
            {
                #region Previously completed parsing or failed to parse -> reset to prepare for new parsing

                Reset();

                #endregion
            }
            #region Defensive tracing
            else
            {
                //not in special states, move on
            }
            #endregion

            #endregion

            //process
            ProcessState(reader, mCurrentContent);

            #region Post-processing

            if (mState == HttpParserState.Completed)
            {
                #region Successfully parsed

                content = mCurrentContent;
                result = true;

                #endregion
            }
            else if (mState == HttpParserState.Error)
            {
                #region Failed to parse

                error = new HttpParsingException()
                {
                    FailureReason = mCurrentReason
                };

                #endregion
            }
            else
            {
                #region Unknown -> skip

                //throw new Exception(string.Format("HTTP Parser - Unknown state in post-processing: {0}", mState));

                #endregion
            }

            #endregion

            content = mCurrentContent;

            return result;
        }
        #endregion

        #region On Reset
        /// <summary>
        /// Reset to initial state.
        /// </summary>
        protected virtual void OnReset() { }
        #endregion

        #region Reset
        /// <summary>
        /// Reset to initial state.
        /// </summary>
        public void Reset()
        {
            mState = HttpParserState.Ready;
            mCurrentLineSize = 0;
            mCurrentHeadersCount = 0;
            mDataBytesLeft = 0;
            OnReset();
        }
        #endregion

        #region Constructors
        /// <summary>
        /// Default constructor.
        /// </summary>
        public HttpParserBase()
        { }
        #endregion
    }
}
